﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Forums.SyncService;
using Microsoft.Forums.ForumsProvider;
using System.Threading;
using System.Diagnostics;
using Microsoft.Forums.OfflineStore;

namespace Microsoft.Forums.ForumsClient.Models
{
    /// <summary>
    /// Manages synchronization of forums.
    /// This classes acts as a proxy over the Synchronization controller.  It adds functionality such as
    /// autosynchronization.
    /// </summary>
    public class SynchronizationManager : BindableObject, IDisposable
    {
        #region Private Members
        /// <summary>
        /// Synchronization controller 
        /// </summary>
        ISynchronizationController syncController;

        /// <summary>
        /// Is the scheduler enabled
        /// </summary>
        bool isSchedulerEnabled;

        /// <summary>
        /// The list of forums currently being synchronized
        /// </summary>
        List<ForumFolderViewModel> syncInProgressForums;

        /// <summary>
        /// Collection of forums to auto sync
        /// </summary>
        List<ForumFolderViewModel> autoSyncForums;

        /// <summary>
        ///How often (in seconds) the scheduler should wake up and check the list of forums
        /// </summary>
        int schedulerPeriod;

        /// <summary>
        /// How long (in seconds) before the scheduler initially runs
        /// </summary>
        int schedulerStartDelay;

        /// <summary>
        /// How long (in seconds) since a forums last sychronization should it be synchronized again
        /// </summary>
        int syncFrequency;

        /// <summary>
        /// How many forums can be synchronized simultaneously
        /// </summary>
        int schedulerSynchronizationLimit;

        /// <summary>
        /// The timer which periodically launched the scheduler function
        /// </summary>
        Timer autoSyncTimer;

        /// <summary>
        /// Set to true while inside a run of the scheduler
        /// </summary>
        bool isSchedulerRunning;

        /// <summary>
        /// Object by the scheduler to lock on to ensure only one scheduler is running at a time
        /// </summary>
        object schedulerCollisionLock = new object();

        /// <summary>
        /// Object for synchronization functions to lock onto
        /// </summary>
        object forumOperationLock = new object();

        #endregion

        /// <summary>
        /// Event fired after each run of the schedler
        /// </summary>
        public event EventHandler<EventArgs> SchedulerRunCompleted;

        #region Public Properties
        /// <summary>
        ///  Gets the ISynchronizationController controller.  If not created yet, will create an isntance of
        ///  a SynchronizationController
        /// </summary>
        public ISynchronizationController Controller
        {
            get
            {
                if (syncController == null)
                {
                    syncController = new SynchronizationController(
                        AppDataModel.Instance.ConnectionPool,
                        new List<IForumServiceProvider> { AppDataModel.Instance.ServiceProvider });
                    InitializeEventHandling();
                }

                return syncController;
            }
        }

        /// <summary>
        /// Get or Set how often (in seconds) the scheduler should wake up and check the list of forums
        /// </summary>
        public int SchedulerPeriod
        {
            get
            {
                return schedulerPeriod;
            }
            set
            {
                if (value >= 0)
                {
                    schedulerPeriod = value;
                }
            }
        }


        /// <summary>
        /// Get or Set how long (in seconds) before the scheduler initially runs
        /// </summary>
        public int SchedulerStartDelay
        {
            get
            {
                return schedulerStartDelay;
            }
            set
            {
                if (value >= 0)
                {
                    schedulerStartDelay = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets how long (in seconds) since a forums last sychronization should it
        /// be synchronized again
        /// </summary>
        /// <value>The sync frequency.</value>
        public int SyncFrequency
        {
            get
            {
                return syncFrequency;
            }
            set
            {
                if (value > 0)
                {
                    syncFrequency = value;
                }
            }
        }

        /// <summary>
        /// Gets or Sets how many forums can be synchronized simultaneously
        /// </summary>
        public int SchedulerSynchronizationLimit
        {
            get
            {
                return schedulerSynchronizationLimit;
            }
            set
            {
                if (value > 0)
                {
                    schedulerSynchronizationLimit = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the scheduler which performs auto sync enabled.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if the scheduler is enabled; otherwise, <c>false</c>.
        /// </value>
        public bool IsSchedulerEnabled
        {
            get
            {
                return isSchedulerEnabled;
            }
            set
            {
                if (isSchedulerEnabled != value)
                {
                    isSchedulerEnabled = value;

                    if (isSchedulerEnabled)
                    {
                        StartScheduler();
                    }
                    else
                    {
                        StopScheduler();
                    }

                    OnPropertyChanged("IsSchedulerEnabled");
                }
            }
        }

        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="SynchronizationManager"/> class.
        /// </summary>
        public SynchronizationManager()
        {
            Initialize();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SynchronizationManager"/> class.
        /// This constructor is used for dependency injection for unit tests
        /// </summary>
        /// <param name="controller">The controller.</param>
        /// <param name="forums">The forums.</param>
        public SynchronizationManager(ISynchronizationController controller)
        {
            if (controller == null)
                throw new ArgumentNullException("controller", "controller must not be null");

            syncController = controller;
            Initialize();
            InitializeEventHandling();
        }
        #endregion

        #region Public Methods

        /// <summary>
        /// Adds the forum to scheduler.
        /// </summary>
        /// <param name="forumToAdd">The forum to add.</param>
        public void AddForumToScheduler(ForumFolderViewModel forumToAdd)
        {
            if (forumToAdd == null) throw new ArgumentNullException("forumToAdd", "forumToAdd must not be null");
            if (autoSyncForums.Contains(forumToAdd)) throw new ArgumentException("forumToAdd has already been added", "forumToAdd");

            lock (autoSyncForums)
            {
                autoSyncForums.Add(forumToAdd);
            }
        }

        /// <summary>
        /// Removes the forum from scheduler.
        /// </summary>
        /// <param name="forumToRemove">The forum to remove.</param>
        public void RemoveForumFromScheduler(ForumFolderViewModel forumToRemove)
        {
            if (forumToRemove == null) throw new ArgumentNullException("forumToRemove", "forumToRemove must not be null");
            if (!autoSyncForums.Contains(forumToRemove)) throw new ArgumentException("forumToRemove does not exist in scheduler", "forumToRemove");

            lock (autoSyncForums)
            {
                autoSyncForums.Remove(forumToRemove);
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Common initialization
        /// </summary>
        private void Initialize()
        {
            autoSyncForums = new List<ForumFolderViewModel>();
            syncInProgressForums = new List<ForumFolderViewModel>();

            // Default values 
            SchedulerSynchronizationLimit = Environment.ProcessorCount * 4;
            SyncFrequency = 60 * 60;
            SchedulerPeriod = 25;
            SchedulerStartDelay = 15;
        }

        /// <summary>
        /// Initializes the event handling.
        /// </summary>
        private void InitializeEventHandling()
        {
            Controller.SynchronizeForumCompleted += new EventHandler<SynchronizeForumCompletedEventArgs>(OnSynchronizeForumComplete);
            Controller.SynchronizeThreadCompleted += new EventHandler<SynchronizeThreadCompletedEventArgs>(OnSynchronizeThreadComplete);
        }

        /// <summary>
        /// Called when a scheduler run is completed.
        /// </summary>
        private void OnSchedulerRunCompleted()
        {
            if (SchedulerRunCompleted != null)
            {
                SchedulerRunCompleted(this, new EventArgs());
            }
        }

        /// <summary>
        /// Starts the scheduler.
        /// </summary>
        private void StartScheduler()
        {
            if (autoSyncTimer == null)
            {
                autoSyncTimer = new Timer(Scheduler, null, SchedulerStartDelay * 1000, SchedulerPeriod * 1000);
            }
        }

        /// <summary>
        /// Stops the scheduler.
        /// </summary>
        private void StopScheduler()
        {
            if (autoSyncTimer != null)
            {
                autoSyncTimer.Dispose();
                autoSyncTimer = null;
            }
        }

        /// <summary>
        /// Invoked by the timer to execute one run of the scheduler
        /// This function takes care to make sure its the only scheduler running at a time
        /// </summary>
        /// <param name="state">The state.</param>
        private void Scheduler(object state)
        {
            if (isSchedulerRunning) return;
            lock (schedulerCollisionLock)
            {
                if (isSchedulerRunning) return;
                isSchedulerRunning = true;
            }

            // Check for network connection and that the user is authenticated
            bool onlineAndLoggedOn = AppDataModel.Instance.CurrentUser.LogOnState == LogOnStatus.Authenticated &&
                                    AppDataModel.Instance.Network.IsConnected;

            // Determine how many threads we are allowed to launch to synchronize (one thread per forum)
            // If none we return 
            // There is a chance here that syncInProgressForums is modified during the run of this function
            // However it is only going to be made smaller from outside this function.
            int amountToSync = SchedulerSynchronizationLimit - syncInProgressForums.Count;
            if (amountToSync > 0 && onlineAndLoggedOn)
            {
                DateTime runStartTime = DateTime.Now.ToUniversalTime();
                List<ForumFolderViewModel> syncForums = null;

                // Copy the auto sync forums collection, ensure we have exclusive access while we make a copy of it
                lock (autoSyncForums)
                {
                    syncForums = new List<ForumFolderViewModel>(autoSyncForums);
                }

                // Sort the copy
                syncForums.Sort(CompareForumsToSynchornizeByPriority);

                foreach (var forum in syncForums)
                {
                    if (forum.IsSyncInProgress)
                    {
                        // The forum is already synchornizing so skip it
                        continue;
                    }

                    if (amountToSync <= 0 ||
                        (forum.Forum.ForumPropertySet.LastSyncedOn != null &&
                         forum.Forum.ForumPropertySet.LastSyncedOn.Value.AddSeconds(SyncFrequency) > runStartTime))
                    {
                        // At this point we can stop checking more forums for this call of Schedler since we either
                        // have no more slots available to sync or none of the forums left in the list are ready
                        // to be synchornized
                        break;
                    }
                    else
                    {
                        try
                        {
                            lock (forumOperationLock)
                            {
                                Controller.SynchronizeForumAsync(forum.Forum.Id);
                                forum.IsSyncInProgress = true;
                                amountToSync--;
                                syncInProgressForums.Add(forum);
                            }
                        }
                        catch (SynchronizationInProgressException)
                        {
                            Debug.WriteLine("User synchronized forum right before Auto-sync was able to for forum: " + forum.Forum.DisplayName);
                        }
                        catch (ForumNotInDatabaseException)
                        {
                            Debug.WriteLine("The forum has been removed from the database: " + forum.Forum.DisplayName);
                        }
                    }
                }
            }
            lock (schedulerCollisionLock)
            {
                isSchedulerRunning = false;
            }

            OnSchedulerRunCompleted();
        }

        /// <summary>
        /// Compares the forums to synchornize by priority.
        /// The lower the the 
        /// </summary>
        /// <param name="f1">The f1.</param>
        /// <param name="f2">The f2.</param>
        /// <returns> 
        /// < 0 -> f1 < f2
        /// 0 -> f1 Equals f2        
        /// > 0 -> f1 > f2        
        /// </returns>
        private static int CompareForumsToSynchornizeByPriority(ForumFolderViewModel f1, ForumFolderViewModel f2)
        {
            DateTime? date1 = f1.Forum.ForumPropertySet.LastSyncedOn;
            DateTime? date2 = f2.Forum.ForumPropertySet.LastSyncedOn;

            if (date1 == null)
            {
                if (date2 == null)
                {
                    // If both are null then they are same priority
                    return 0;
                }
                else
                {
                    // date1 is null but date2 is not so date1 is less
                    return -1;
                }
            }
            else
            {
                if (date2 == null)
                {
                    // date2 is null but date1 is not so date2 is less
                    return 1;
                }
                else
                {
                    // Both are not null so compare the datetime values
                    return date1.Value.CompareTo(date2.Value);
                }
            }

        }

        /// <summary>
        /// Called when the complete synchronization finishes.  At this point we update that forum object
        /// and update the forum to no longer be in a syncyhronizing state
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="Microsoft.Forums.SyncService.SynchronizeForumCompletedEventArgs"/> instance containing the event data.</param>
        private void OnSynchronizeForumComplete(object sender, SynchronizeForumCompletedEventArgs e)
        {

            var forumId = e.ForumId;
            if (e.Error == null)
            {
                if (e.SynchronizeForumSummary.SynchronizeType == SynchronizeType.Update)
                {
                    ObjectRegistry.UpdateForum(forumId);
                }
            }
            else
            {
                Debug.WriteLine("An error occured during synchronizing forum id: " + forumId);
                Debug.WriteLine(e.Error);
                //Some Error Occured
            }

            var fc = AppDataModel.Instance.SubscribedForums[forumId];
            if (fc != null)
            {
                lock (forumOperationLock)
                {
                    fc.IsSyncInProgress = false;

                    if (syncInProgressForums.Any(f => f.Forum.Id == forumId))
                    {
                        syncInProgressForums.Remove(fc);
                    }
                }
            }
        }

        /// <summary>
        /// Called when a thread has been synchronized.  At this point update that thread and its posts in memory.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="Microsoft.Forums.SyncService.SynchronizeThreadCompletedEventArgs"/> instance containing the event data.</param>
        private void OnSynchronizeThreadComplete(object sender, SynchronizeThreadCompletedEventArgs e)
        {
            var forumId = e.ForumId;
            if (e.Error == null)
            {
                var threadSyncSummary = e.SynchronizeThreadSummary;
                bool postsHaveBeenAdded = false;

                // Add or Update the thread object
                if (threadSyncSummary.SynchronizeType == SynchronizeType.Update)
                {
                    ObjectRegistry.UpdateThread(forumId, threadSyncSummary.ThreadId);
                }
                else
                {
                    Forum forum = ObjectRegistry.GetForum(forumId);
                    forum.OnThreadAdded(threadSyncSummary.ThreadId);
                }

                // Add or update each post object
                foreach (var syncPostSummary in threadSyncSummary.PostSummaries)
                {
                    if (syncPostSummary.SynchronizeType == SynchronizeType.Update)
                    {
                        ObjectRegistry.UpdatePost(forumId, syncPostSummary.PostId);
                    }
                    else
                    {
                        Forum forum = ObjectRegistry.GetForum(forumId);
                        forum.OnPostAdded(syncPostSummary.PostId);

                        Post addedPost = ObjectRegistry.GetPost(forumId, syncPostSummary.PostId);
                        if (addedPost.ParentId != null)
                        {
                            Post parentPost = ObjectRegistry.GetPost(forumId, (Guid)addedPost.ParentId);
                            if (parentPost != null)
                            {
                                parentPost.OnChildAdded(addedPost.Id);
                            }
                        }

                        if (addedPost.CreatedById == AppDataModel.Instance.CurrentUser.UserInfo.Id)
                        {
                            AppDataModel.Instance.CurrentUser.UserInfo.OnUpdatedUserThread(addedPost.ForumId, addedPost.ThreadId);
                        }
                        postsHaveBeenAdded = true;
                    }
                }

                if (postsHaveBeenAdded)
                {
                    var forumModel = AppDataModel.Instance.SubscribedForums[forumId];
                    if (forumModel != null)
                    {
                        forumModel.RefreshPostCounts();
                    }
                }
            }
            else
            {
                Debug.WriteLine("An error occured during synchronizing forum id: " + forumId);
                Debug.WriteLine(e.Error);
                //Some Error Occured
            }
        }
        #endregion


        #region IDisposable Members

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }


        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Dispose managed resources.
                StopScheduler();
            }
        }
        #endregion
    }
}
